# pita: An invisible audio player
# Copyright (C) 2006-2007 Austin Bingham
#
# This program is free software# you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation# either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY# without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program# if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# You can reach the author at:
#     austin.bingham@gmail.com

from threading import *
import urllib
import pyxiph.ao


import decoder, playlist
import logging, time
import pita

logger = logging.getLogger('pita.player')

def setter(notify=True):
    def dec(f):
        def wrapper(self, *args, **kwargs):
            self.lock.acquire()
            try:
                f(self, *args, **kwargs)
                if notify: self.cond.notify()
            finally:
                self.lock.release()
        wrapper.__name__ = f.__name__
        wrapper.__dict__ = f.__dict__
        wrapper.__doc__ = f.__doc__
        return wrapper
    return dec

def getter(f):
    def wrapper(self, *args, **kwargs):
        self.lock.acquire()
        try:
            return f(self, *args, **kwargs)
        finally:
            self.lock.release()
    wrapper.__name__ = f.__name__
    wrapper.__dict__ = f.__dict__
    wrapper.__doc__ = f.__doc__
    return wrapper
        
class Player(Thread):
    def __init__(self, yield_time=0.001):
        Thread.__init__(self)

        logger.info('yield_time = %f' % yield_time)

        self.yield_time = yield_time
        
        self.playlist = playlist.Playlist()
        self.curr_decoder = None # decoder currently being used

        self._sample_rate = pyxiph.ao.Format.default_rate
        
        self.lock = Lock() # protects access to playlist and current
        self.cond = Condition(self.lock)

        self._paused = False
        self.terminate = False

    @setter()
    def set_paused(self, p):
        self._paused = p

    @getter
    def is_paused(self):
        return self._paused

    paused = property(is_paused, set_paused,
                      'Controls the pause state of the player')

    @setter()
    def set_random(self, r):
        self.playlist.random = r

    @getter
    def is_random(self):
        return self.playlist.random

    random = property(is_random, set_random,
                      'Controls the random state of the player')

    @setter()
    def set_looping(self, l):
        self.playlist.looping = l

    @getter
    def is_looping(self):
        return self.playlist.looping

    looping = property(is_looping, set_looping,
                       'Controls the looping state of the player')

    @setter()
    def set_sample_rate(self, r):
        self._sample_rate = r
        if self.curr_decoder:
            self.curr_decoder.sample_rate = r

    @getter
    def get_sample_rate(self):
        return self._sample_rate

    sample_rate = property(get_sample_rate, set_sample_rate,
                           'controls sample rate of player (samples/sec)')

    @setter()
    def append(self, filenames):
        for filename in filenames:
            try:
                self.playlist.append(playlist.Entry(filename))
            except IOError:
                logging.error('unable to create playlist.Entry for %s' % filename)

    @setter()
    def remove(self, pos):
        if self.playlist.remove(pos):
            self.curr_decoder = None

    # TODO: prepend, insert

    @setter()
    def clear(self):
        self.playlist.clear()
        self.curr_decoder = None

    @getter
    def get_playlist(self):
        return self.playlist

    @setter()
    def next(self):
        self.playlist.next()
        self.curr_decoder = None

    @setter()
    def previous(self):
        if self.playlist.previous():
            self.curr_decoder = None

    @setter()
    def jump_to(self, pos):
        try:
            self.playlist.jump_to(pos)
            self.curr_decoder = None
        except IndexError:
            pass

    def at_end(self):
        assert self.playlist.current >= 0, 'Player.current less than zero'
        return self.playlist.current >= len(self.playlist)

    @setter()
    def finish(self):
        self.terminate = True

    def get_decoder(self, entry):
        try:
            return pita.decoder_map[entry.mime_type](urllib.urlopen(entry.filename))
        except KeyError:
            logger.warning('no decoder found for %s. Assuming mp3.' % entry.filename)
            return pita.default_decoder(urllib.urlopen(entry.filename))
        except IOError:
            logger.warning('error calling urlopen with %s' % entry.filename)
        return None

    def run(self):
        while True:
            self.lock.acquire()

            # First, compare what we're doing now (i.e. what we're playing,
            # if we're paused, etc) with the Player state. Update this thread's
            # state to match
            try:
                if self.terminate: return
            
                while self._paused or self.at_end():
                    logger.info('paused=%s, at_end=%d' % (self._paused, self.at_end()))
                    logger.info('waiting on condition')
                    self.cond.wait()
                    if self.terminate: return

                assert self.playlist.current >= 0, 'Player.current less than zero'
                assert self.playlist.current < len(self.playlist), 'Player.current outside playlist'

            finally:
                self.lock.release()

            # Now we've updated to the Player's state. Make sure the
            # correct decoder is 
            if not self.curr_decoder:
                self.curr_decoder = self.get_decoder(self.playlist[self.playlist.current])
                
            if not self.curr_decoder:
                logger.warning('no decoder found for %s' % self.playlist[self.playlist.current])
                self.next()
            else:
                rslt = self.curr_decoder.play_chunk()
                if rslt == decoder.Decoder.FINISHED:
                    logger.info('finished playing %s' % self.playlist[self.playlist.current])
                    self.next()
                elif rslt == decoder.Decoder.ERROR:
                    logger.error('error decoding %s' % self.playlist[self.playlist.current])
                    self.next()

            # This simulates a 'yield' call which is mysteriously missing from
            # python's threading system
            time.sleep(self.yield_time)
